/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package elaborate.h2x.transform.position.schema.statematcher;

import elaborate.h2x.parser.QNameUtil;
import elaborate.h2x.transform.TransformingModel;
import elaborate.h2x.transform.TransformingNode;
import elaborate.h2x.transform.TransformingNodeId;
import elaborate.h2x.transform.position.schema.state.ElementState;
import elaborate.h2x.transform.position.schema.state.PseudoState;
import elaborate.h2x.transform.position.schema.state.State;
import elaborate.h2x.transform.position.schema.state.Transition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.namespace.QName;

/**
 *
 * @author lendle
 */
public class DefaultStateMatcherImpl implements StateMatcher{
    
    public List<TransformingNodeId> matchState(PseudoState pseudoState, TransformingModel model, List<TransformingNodeId> childNodes) {
        List<TransformingNodeId> ret=new ArrayList<TransformingNodeId>();
        //collect QName-->transforming node id mappings
        //for faster access
        Map<QName, List<TransformingNodeId>> childNodeMap=new HashMap<QName, List<TransformingNodeId>>();
        for(TransformingNodeId id : childNodes){
            TransformingNode node=model.getNode(id);
            if(node.getRelationList().isEmpty()==false){
                //explicitly positioned nodes are excluded
                continue;
            }
            QName qName=QNameUtil.getQName(node.getDef().getNode());
            //System.out.println("\t"+qName);
            List<TransformingNodeId> orderedList=childNodeMap.get(qName);
            if(orderedList==null){
                orderedList=new ArrayList<TransformingNodeId>();
                childNodeMap.put(qName, orderedList);
            }
            orderedList.add(id);
        }
        Set<State> firstSet=pseudoState.getFirstSet();
        for(State state : firstSet){
            ElementState elementState=(ElementState) state;
            Result result=this.matchElementState(null, elementState, model, childNodeMap);
            List<TransformingNodeId> matchingSequence=result.getConsumedNodeIds();
            //System.out.println("\t"+result.getStopAtState());
            if(this.isEmptyRemainingChildNodeMap(childNodeMap)){
                ret.addAll(matchingSequence);
                break;
            }
        }
        return ret;
    }
    /**
     * start matching from the given current state
     * and follow transition arcs to consume remaining
     * child nodes (node ids).
     * during execution, the method may fork once encountering
     * multiple transition arcs. before forking the method keeps
     * nodes consumed within the current session for possible
     * backtracking.
     * @param prevState
     * @param currentState
     * @param model
     * @param remainingChildNodeMap
     * @return 
     */
    protected Result matchElementState(ElementState prevState, ElementState currentState, TransformingModel model, Map<QName, List<TransformingNodeId>> remainingChildNodeMap){
        List<TransformingNodeId> consumedNodeIds=new ArrayList<TransformingNodeId>();
        //System.out.println("\texpectedQName="+currentState.getElementNode().getQName());
        if(!this.matchQName(currentState.getElementNode().getQName(), consumedNodeIds, model, remainingChildNodeMap)){
            return new Result(consumedNodeIds, prevState);
        }
        List<Transition> possibleTransitions = currentState.getTransitions();
        //System.out.println("\tpossibleTransitions="+possibleTransitions.size());
        while(possibleTransitions.size()==1){
            //there is only one transition, backtracking not needed
            Transition currentTransition=possibleTransitions.get(0);
            QName expectedQName=currentTransition.getQName();
            //System.out.println("\texpectedQName="+expectedQName);
            if(!this.matchQName(expectedQName, consumedNodeIds, model, remainingChildNodeMap)){
                return new Result(consumedNodeIds, currentState);
            }
            currentState=(ElementState) currentTransition.getState();
            possibleTransitions = currentState.getTransitions();
        }
        ElementState nextState=null;
        if(possibleTransitions==null || possibleTransitions.isEmpty()){
            //no further comsumption possible
            return new Result(consumedNodeIds, currentState);
        }
        else{
            //backtracking may be needed
            for(Transition transition : possibleTransitions){
                nextState=(ElementState) transition.getState();
                List<TransformingNodeId> nextConsumedNodeIds=this.matchElementState(currentState, nextState, model, remainingChildNodeMap).getConsumedNodeIds();
                if(isEmptyRemainingChildNodeMap(remainingChildNodeMap)){
                    //this branch consumed all node ids
                    //should also check if valid
                    consumedNodeIds.addAll(nextConsumedNodeIds);
                    return new Result(consumedNodeIds, nextState);
                }
                else{
                    //this brach fail to consume remaining node ids
                    //backtrack
                    for (int i = nextConsumedNodeIds.size() - 1; i >= 0; i--) {
                        TransformingNodeId _id = nextConsumedNodeIds.get(i);
                        remainingChildNodeMap.get(QNameUtil.getQName(model.getNode(_id).getDef().getNode())).add(0, _id);
                    }
                }
            }
        }
        return new Result(consumedNodeIds, currentState);
    }
    
    private boolean matchQName(QName expectedQName, List<TransformingNodeId> consumedNodeIds, TransformingModel model, Map<QName, List<TransformingNodeId>> remainingChildNodeMap){
        List<TransformingNodeId> matchedNodeIds=remainingChildNodeMap.get(expectedQName);
        if(matchedNodeIds==null){
            //no need to process this node
            //the node is explicitly positioned
            return true;
        }
        if(matchedNodeIds.isEmpty()==false){
            //this state can be matched
            TransformingNodeId id=matchedNodeIds.remove(0);
            consumedNodeIds.add(id);
            //System.out.println("\texpectedQName="+expectedQName+", "+true);
            return true;
        }
        else{
            //System.out.println("\texpectedQName="+expectedQName+", "+false);
            return false;
        }
    }
    /**
     * an empty child node map is a map with no remaining 
     * transforming node ids for any given QName.
     * @param remainingChildNodeMap
     * @return 
     */
    private boolean isEmptyRemainingChildNodeMap(Map<QName, List<TransformingNodeId>> remainingChildNodeMap){
        boolean allConsumed = true;
        for (List<TransformingNodeId> list : remainingChildNodeMap.values()) {
            if (list.isEmpty() == false) {
                allConsumed = false;
                break;
            }
        }
        return allConsumed;
    }
    
    static class Result{

        protected List<TransformingNodeId> consumedNodeIds = new ArrayList<TransformingNodeId>();
        protected State stopAtState;

        public Result(List<TransformingNodeId> consumedNodeIds, State stopAtState) {
            this.stopAtState = stopAtState;
            this.consumedNodeIds.addAll(consumedNodeIds);
        }

        /**
         * Get the value of stopAtState
         *
         * @return the value of stopAtState
         */
        public State getStopAtState() {
            return stopAtState;
        }

        /**
         * Set the value of stopAtState
         *
         * @param stopAtState new value of stopAtState
         */
        public void setStopAtState(State stopAtState) {
            this.stopAtState = stopAtState;
        }


        /**
         * Get the value of consumedNodeIds
         *
         * @return the value of consumedNodeIds
         */
        public List<TransformingNodeId> getConsumedNodeIds() {
            return new ArrayList<TransformingNodeId>(consumedNodeIds);
        }

        /**
         * Set the value of consumedNodeIds
         *
         * @param consumedNodeIds new value of consumedNodeIds
         */
        public void setConsumedNodeIds(List<TransformingNodeId> consumedNodeIds) {
            consumedNodeIds.clear();
            this.consumedNodeIds.addAll(consumedNodeIds);
        }

    }
}
